package lumis.portal.serialization.persistency.zip;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import lumis.portal.serialization.persistency.IDirectory;
import lumis.portal.serialization.persistency.IFSDirectory;

/**
 * {@link IDirectory} implementation that uses a zip file as the concrete repository.
 * 
 * This implementation allows only one open stream at a time. Tries to get some input or output stream when another stream was got and not closed yet will raise an {@link StreamStillOpenException}.
 * 
 * @version $Revision: 7234 $ $Date: 2007-06-06 00:26:02 -0300 (Wed, 06 Jun 2007) $
 * @since 4.0.10
 */
public class ZipDirectory implements IFSDirectory
{
	private File file = null;
	private ZipFile zipFile = null;
	private boolean closed = false;
	private ZipOutputStream zos = null;
	private InputStream inputStream = null;
	private OutputStream outputStream = null;
	
	/**
	 * @param zipFilePath path to the zip file.
	 * @throws FileNotFoundException if the zip file was not found. 
	 */
	public ZipDirectory(String zipFilePath) throws FileNotFoundException
	{
		this(new File(zipFilePath));
	}
	
	/**
	 * @param zipFile {@link File} that wraps the zip file.
	 * @throws FileNotFoundException if the zip file was not found.
	 */
	public ZipDirectory(File zipFile) throws FileNotFoundException
	{
		if (!zipFile.exists())
		{
			throw new FileNotFoundException();
		}
		file = zipFile;
	}

	/**
	 * @param zipFile {@link File} that wraps the zip file.
	 * @param createIfNotExist if true, the zip file will be created if it does not exist. 
	 * @throws FileNotFoundException if the zip file was not found and createIfNotExist=false.
	 * @throws IOException
	 */
	public ZipDirectory(File zipFile, boolean createIfNotExist) throws FileNotFoundException, IOException
	{
		if (!zipFile.exists())
		{
			if (!createIfNotExist)
			{
				throw new FileNotFoundException();
			}
			else
			{
				File parentFile = zipFile.getParentFile();
				if (!parentFile.exists() && !parentFile.mkdirs())
					throw new IllegalStateException("Error creating dirs to '" + zipFile.getAbsolutePath() + "'");

				zipFile.createNewFile();
			}
		}
		file = zipFile;
	}
	
	/**
	 * {@inheritDoc}
	 * @throws IllegalStateException if this method is called in an already closed instance.
	 */
	public boolean exists(String filePath) throws IOException
	{
		ensureOpen();
		ensureNoOpenedStreams();
		
		// switch to "read mode"
		closeZipOutputStream();
		openZipFile();

		return zipFile.getEntry(filePath) != null;
	}

	public String getDescription()
	{
		return this.getClass().getName() + " zipPath=" + file.getAbsolutePath();
	}

	/**
	 * {@inheritDoc}
	 * <p>This implementation does not allow more than one stream open at a time.   
	 * @throws StreamStillOpenException if there is another stream that was gotten and not closed yet.
	 * @throws IllegalStateException if this method is called in an already closed instance.
	 */
	public synchronized InputStream getInputStream(String filePath) throws StreamStillOpenException, FileNotFoundException, IOException
	{
		ensureOpen();
		ensureNoOpenedStreams();
		
		// switch to "read mode"
		closeZipOutputStream();
		openZipFile();
		
		// get the zip entry
		ZipEntry zipEntry = zipFile.getEntry(filePath);
		if (zipEntry == null)
			throw new FileNotFoundException();

		// create an input stream to the zip entry
		inputStream = zipFile.getInputStream(zipEntry);

		// create a call back that notify the directory when the input stream is closed
		Closeable streamClosingCallback = new Closeable()
		{
			public void close()
			{
				inputStream = null;
			}
		};

		// return the input stream
		return new ZipDirectoryInputStream(inputStream, streamClosingCallback);
	}

	/**
	 * {@inheritDoc}
	 * <p>Due to an output optimization, this implementation does not persist the new files just after closing the {@link OutputStream}. To commit the persistence, the method {@link ZipDirectory#close()} must be called. 
	 * <p>This implementation does not allow more than one stream open at a time.   
	 * <p>If the file already exists, an {@link IOException} will be thrown.
	 * @throws StreamStillOpenException if there is another stream that was gotten and not closed yet.
	 * @throws IllegalStateException if this method is called in an already closed instance.
	 */
	public synchronized OutputStream getOutputStream(String filePath) throws StreamStillOpenException, IOException
	{
		ensureOpen();
		ensureNoOpenedStreams();

		// switch to "write mode"
		closeZipFile();
		openZipOutputStream();
		
		// create a new zip entry on the output stream
		ZipEntry zipEntry = new ZipEntry(filePath);
		zipEntry.setMethod(ZipEntry.DEFLATED);
		zos.putNextEntry(zipEntry); 
		outputStream = zos;

		// create a call back that notify the directory when the output stream is closed
		Closeable streamClosingCallback = new Closeable()
		{
			public void close()
			{
				outputStream = null;
			}
		};
		
		// return the output stream
		return (OutputStream)new ZipDirectoryOutputStream(outputStream, streamClosingCallback);
	}
	
    /**
     * {@inheritDoc}
     * <p> In this implementation, this method MUST be called in order to persist new added files.  
     */
	public synchronized void close() throws IOException
	{
		if (!closed)
		{
			try
			{
				closeZipFile();
				closeZipOutputStream();
			}
			finally
			{
				closed = true;
			}
		}
	}
	
	public void delete() throws IOException
	{
		close();
		if (!file.delete())
			throw new IOException("Could not delete Directory " + getDescription());
	}

	/**
	 * Open the internal ZipFile, in order to allow read files.
	 * @throws IOException if an I/O error occurs.
	 */
	private void openZipFile() throws IOException
	{
		if (zipFile == null)
			zipFile = new ZipFile(file);
	}

	/**
	 * Close the internal ZipFile.
	 * @throws IOException if an I/O error occurs.
	 */
	private void closeZipFile() throws IOException
	{
		if (zipFile != null)
		{
			try
			{
				zipFile.close();
			}
			finally
			{
				zipFile = null;
			}
		}
	}

	/**
	 * Open the internal ZipOutputStream, in order to allow adding new files.
	 * @throws FileNotFoundException If the zip file was not found.
	 */
	private void openZipOutputStream() throws FileNotFoundException
	{
		if (zos == null)
			zos = new ZipOutputStream(new FileOutputStream(file));
	}

	/**
	 * Close the internal ZipOutputStream.
	 * @throws IOException if an I/O error occurs.
	 */
	private void closeZipOutputStream() throws IOException
	{
		if (zos != null)
		{
			try
			{
				zos.close();
			}
			finally
			{
				zos = null;
			}
		}
	}

	/**
	 * Ensure that the ZipDirectory was not closed.
	 */
	private synchronized void ensureOpen()
	{
		if (closed)
			throw new IllegalStateException("Zip file closed");
	}
		
	/**
	 * Ensure that there is no open input or output streams.
	 * @throws StreamStillOpenException If there is an open input or output stream.
	 */
	private synchronized void ensureNoOpenedStreams() throws StreamStillOpenException
	{
		if (inputStream != null)
			throw new StreamStillOpenException("There is another input stream opened.");
		if (outputStream != null)
			throw new StreamStillOpenException("There is an output stream opened.");
	}

	@Override
	protected void finalize() throws Throwable
	{
		super.finalize();
		close();
	}

	public String getDirectoryPath()
	{
		return file.getAbsolutePath();
	}
}
